/*
 * Copyright (c) 2012, the Dart project authors.
 * 
 * Licensed under the Eclipse Public License v1.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 * 
 * http://www.eclipse.org/legal/epl-v10.html
 * 
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */
package com.google.dart.tools.core.formatter;

import com.google.dart.server.utilities.general.StringUtilities;
import com.google.dart.tools.core.internal.util.ScannerHelper;
import com.google.dart.tools.core.internal.util.Util;

import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.DefaultLineTracker;
import org.eclipse.jface.text.ILineTracker;
import org.eclipse.jface.text.IRegion;
import org.eclipse.text.edits.ReplaceEdit;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Map;

/**
 * The class <code>IndentManipulation</code> is a utility class to provide String manipulation
 * functions dealing with indentations.
 * 
 * @coverage dart.tools.core
 */
public final class IndentManipulation {
  /**
   * Change the indent of a, possible multiple line, code string. The given number of indent units
   * is removed, and a new indent string is added.
   * <p>
   * The first line of the code will not be changed (It is considered to have no indent as it might
   * start in the middle of a line).
   * </p>
   * 
   * @param code the code to change the indent of
   * @param indentUnitsToRemove the number of indent units to remove from each line (except the
   *          first) of the given code
   * @param tabWidth the size of one tab in space equivalents
   * @param indentWidth the width of one indentation unit in space equivalents
   * @param newIndentString the new indent string to be added to all lines (except the first)
   * @param lineDelim the new line delimiter to be used. The returned code will contain only this
   *          line delimiter.
   * @return the newly indent code, containing only the given line delimiters.
   * @exception IllegalArgumentException if:
   *              <ul>
   *              <li>the given <code>indentWidth</code> is lower than zero</li>
   *              <li>the given <code>tabWidth</code> is lower than zero</li>
   *              <li>the given <code>code</code> is null</li>
   *              <li>the given <code>indentUnitsToRemove</code> is lower than zero</li>
   *              <li>the given <code>newIndentString</code> is null</li>
   *              <li>the given <code>lineDelim</code> is null</li>
   *              </ul>
   */
  public static String changeIndent(String code, int indentUnitsToRemove, int tabWidth,
      int indentWidth, String newIndentString, String lineDelim) {
    if (tabWidth < 0 || indentWidth < 0 || code == null || indentUnitsToRemove < 0
        || newIndentString == null || lineDelim == null) {
      throw new IllegalArgumentException();
    }

    try {
      ILineTracker tracker = new DefaultLineTracker();
      tracker.set(code);
      int nLines = tracker.getNumberOfLines();
      if (nLines == 1) {
        return code;
      }

      StringBuffer buf = new StringBuffer();

      for (int i = 0; i < nLines; i++) {
        IRegion region = tracker.getLineInformation(i);
        int start = region.getOffset();
        int end = start + region.getLength();
        String line = code.substring(start, end);

        if (i == 0) { // no indent for first line (contained in the formatted
                      // string)
          buf.append(line);
        } else { // no new line after last line
          buf.append(lineDelim);
          buf.append(newIndentString);
          if (indentWidth != 0) {
            buf.append(trimIndent(line, indentUnitsToRemove, tabWidth, indentWidth));
          } else {
            buf.append(line);
          }
        }
      }
      return buf.toString();
    } catch (BadLocationException e) {
      // can not happen
      return code;
    }
  }

  /**
   * Returns the leading indentation string of the given line. Note that the returned string need
   * not be equal to the leading whitespace as odd spaces are not considered part of the
   * indentation.
   * 
   * @param line the line to scan
   * @param tabWidth the size of one tab in space equivalents
   * @param indentWidth the width of one indentation unit in space equivalents
   * @return the indent part of <code>line</code>, but no odd spaces
   * @exception IllegalArgumentException if:
   *              <ul>
   *              <li>the given <code>indentWidth</code> is lower than zero</li>
   *              <li>the given <code>tabWidth</code> is lower than zero</li>
   *              <li>the given <code>line</code> is null</li>
   *              </ul>
   */
  public static String extractIndentString(String line, int tabWidth, int indentWidth) {
    if (tabWidth < 0 || indentWidth < 0 || line == null) {
      throw new IllegalArgumentException();
    }

    int size = line.length();
    int end = 0;

    int spaceEquivs = 0;
    int characters = 0;
    for (int i = 0; i < size; i++) {
      char c = line.charAt(i);
      if (c == '\t') {
        spaceEquivs = calculateSpaceEquivalents(tabWidth, spaceEquivs);
        characters++;
      } else if (isIndentChar(c)) {
        spaceEquivs++;
        characters++;
      } else {
        break;
      }
      if (spaceEquivs >= indentWidth) {
        end += characters;
        characters = 0;
        if (indentWidth == 0) {
          spaceEquivs = 0;
        } else {
          spaceEquivs = spaceEquivs % indentWidth;
        }
      }
    }
    if (end == 0) {
      return Util.EMPTY_STRING;
    } else if (end == size) {
      return line;
    } else {
      return line.substring(0, end);
    }
  }

  /**
   * Returns the text edits retrieved after changing the indentation of a, possible multi-line, code
   * string.
   * <p>
   * The given number of indent units is removed, and a new indent string is added.
   * </p>
   * <p>
   * The first line of the code will not be changed (It is considered to have no indent as it might
   * start in the middle of a line).
   * </p>
   * 
   * @param source The code to change the indent of
   * @param indentUnitsToRemove the number of indent units to remove from each line (except the
   *          first) of the given code
   * @param tabWidth the size of one tab in space equivalents
   * @param indentWidth the width of one indentation unit in space equivalents
   * @param newIndentString the new indent string to be added to all lines (except the first)
   * @return returns the resulting text edits
   * @exception IllegalArgumentException if:
   *              <ul>
   *              <li>the given <code>indentWidth</code> is lower than zero</li>
   *              <li>the given <code>tabWidth</code> is lower than zero</li>
   *              <li>the given <code>source</code> is null</li>
   *              <li>the given <code>indentUnitsToRemove</code> is lower than zero</li>
   *              <li>the given <code>newIndentString</code> is null</li>
   *              </ul>
   */
  public static ReplaceEdit[] getChangeIndentEdits(String source, int indentUnitsToRemove,
      int tabWidth, int indentWidth, String newIndentString) {
    if (tabWidth < 0 || indentWidth < 0 || source == null || indentUnitsToRemove < 0
        || newIndentString == null) {
      throw new IllegalArgumentException();
    }

    ArrayList<ReplaceEdit> result = new ArrayList<ReplaceEdit>();
    try {
      ILineTracker tracker = new DefaultLineTracker();
      tracker.set(source);
      int nLines = tracker.getNumberOfLines();
      if (nLines == 1) {
        return result.toArray(new ReplaceEdit[result.size()]);
      }
      for (int i = 1; i < nLines; i++) {
        IRegion region = tracker.getLineInformation(i);
        int offset = region.getOffset();
        String line = source.substring(offset, offset + region.getLength());
        int length = indexOfIndent(line, indentUnitsToRemove, tabWidth, indentWidth);
        if (length >= 0) {
          result.add(new ReplaceEdit(offset, length, newIndentString));
        } else {
          length = measureIndentUnits(line, tabWidth, indentWidth);
          result.add(new ReplaceEdit(offset, length, StringUtilities.EMPTY));
        }
      }
    } catch (BadLocationException cannotHappen) {
      // can not happen
    }
    return result.toArray(new ReplaceEdit[result.size()]);
  }

  /**
   * Returns the tab width as configured in the given map.
   * <p>
   * Use {@link DartProject#getOptions(boolean)} to get the most current project options.
   * </p>
   * 
   * @param options the map to get the formatter settings from
   * @return the indent width
   * @exception IllegalArgumentException if the given <code>options</code> is null
   */
  public static int getIndentWidth(Map<String, String> options) {
    if (options == null) {
      throw new IllegalArgumentException();
    }
    int tabWidth = getTabWidth(options);
    boolean isMixedMode = DefaultCodeFormatterConstants.MIXED.equals(options.get(DefaultCodeFormatterConstants.FORMATTER_TAB_CHAR));
    if (isMixedMode) {
      return getIntValue(
          options,
          DefaultCodeFormatterConstants.FORMATTER_INDENTATION_SIZE,
          tabWidth);
    }
    return tabWidth;
  }

  /**
   * Returns the tab width as configured in the given map.
   * <p>
   * Use {@link DartProject#getOptions(boolean)} to get the most current project options.
   * </p>
   * 
   * @param options the map to get the formatter settings from.
   * @return the tab width
   * @exception IllegalArgumentException if the given <code>options</code> is null
   */
  public static int getTabWidth(Map<String, String> options) {
    if (options == null) {
      throw new IllegalArgumentException();
    }
    return getIntValue(options, DefaultCodeFormatterConstants.FORMATTER_TAB_SIZE, 4);
  }

  /**
   * Returns <code>true</code> if the given character is an indentation character. Indentation
   * character are all whitespace characters except the line delimiter characters.
   * 
   * @param ch the given character
   * @return Returns <code>true</code> if this the character is a indent character,
   *         <code>false</code> otherwise
   */
  public static boolean isIndentChar(char ch) {
    return ScannerHelper.isWhitespace(ch) && !isLineDelimiterChar(ch);
  }

  /**
   * Returns <code>true</code> if the given character is a line delimiter character.
   * 
   * @param ch the given character
   * @return Returns <code>true</code> if this the character is a line delimiter character,
   *         <code>false</code> otherwise
   */
  public static boolean isLineDelimiterChar(char ch) {
    return ch == '\n' || ch == '\r';
  }

  /**
   * Returns the indentation of the given line in space equivalents.
   * <p>
   * Tab characters are counted using the given <code>tabWidth</code> and every other indent
   * character as one. This method analyzes the content of <code>line</code> up to the first
   * non-whitespace character.
   * </p>
   * 
   * @param line the string to measure the indent of
   * @param tabWidth the width of one tab in space equivalents
   * @return the measured indent width in space equivalents
   * @exception IllegalArgumentException if:
   *              <ul>
   *              <li>the given <code>line</code> is null</li>
   *              <li>the given <code>tabWidth</code> is lower than zero</li>
   *              </ul>
   */
  public static int measureIndentInSpaces(CharSequence line, int tabWidth) {
    if (tabWidth < 0 || line == null) {
      throw new IllegalArgumentException();
    }

    int length = 0;
    int max = line.length();
    for (int i = 0; i < max; i++) {
      char ch = line.charAt(i);
      if (ch == '\t') {
        length = calculateSpaceEquivalents(tabWidth, length);
      } else if (isIndentChar(ch)) {
        length++;
      } else {
        return length;
      }
    }
    return length;
  }

  /**
   * Returns the indentation of the given line in indentation units. Odd spaces are not counted.
   * This method only analyzes the content of <code>line</code> up to the first non-whitespace
   * character.
   * 
   * @param line the string to measure the indent of
   * @param tabWidth the width of one tab character in space equivalents
   * @param indentWidth the width of one indentation unit in space equivalents
   * @return the number of indentation units that line is indented by
   * @exception IllegalArgumentException if:
   *              <ul>
   *              <li>the given <code>indentWidth</code> is lower than zero</li>
   *              <li>the given <code>tabWidth</code> is lower than zero</li>
   *              <li>the given <code>line</code> is null</li>
   *              </ul>
   */
  public static int measureIndentUnits(CharSequence line, int tabWidth, int indentWidth) {
    if (indentWidth < 0 || tabWidth < 0 || line == null) {
      throw new IllegalArgumentException();
    }

    if (indentWidth == 0) {
      return 0;
    }
    int visualLength = measureIndentInSpaces(line, tabWidth);
    return visualLength / indentWidth;
  }

  /**
   * Removes the given number of indentation units from a given line. If the line has less indent
   * than the given indentUnitsToRemove, all the available indentation is removed. If
   * <code>indentsToRemove <= 0 or indent == 0</code> the line is returned.
   * 
   * @param line the line to trim
   * @param tabWidth the width of one tab in space equivalents
   * @param indentWidth the width of one indentation unit in space equivalents
   * @return the trimmed string
   * @exception IllegalArgumentException if:
   *              <ul>
   *              <li>the given <code>indentWidth</code> is lower than zero</li>
   *              <li>the given <code>tabWidth</code> is lower than zero</li>
   *              <li>the given <code>line</code> is null</li>
   *              </ul>
   */
  public static String trimIndent(String line, int indentUnitsToRemove, int tabWidth,
      int indentWidth) {
    if (tabWidth < 0 || indentWidth < 0 || line == null) {
      throw new IllegalArgumentException();
    }

    if (indentUnitsToRemove <= 0 || indentWidth == 0) {
      return line;
    }
    final int spaceEquivalentsToRemove = indentUnitsToRemove * indentWidth;

    int start = 0;
    int spaceEquivalents = 0;
    int size = line.length();
    String prefix = null;
    for (int i = 0; i < size; i++) {
      char c = line.charAt(i);
      if (c == '\t') {
        spaceEquivalents = calculateSpaceEquivalents(tabWidth, spaceEquivalents);
      } else if (isIndentChar(c)) {
        spaceEquivalents++;
      } else {
        // Assert.isTrue(false,
        // "Line does not have requested number of indents");
        start = i;
        break;
      }
      if (spaceEquivalents == spaceEquivalentsToRemove) {
        start = i + 1;
        break;
      }
      if (spaceEquivalents > spaceEquivalentsToRemove) {
        // can happen if tabSize > indentSize, e.g tabsize==8, indent==4,
        // indentsToRemove==1, line prefixed with one tab
        // this implements the third option
        start = i + 1; // remove the tab
        // and add the missing spaces
        char[] missing = new char[spaceEquivalents - spaceEquivalentsToRemove];
        Arrays.fill(missing, ' ');
        prefix = new String(missing);
        break;
      }
    }
    String trimmed;
    if (start == size) {
      trimmed = Util.EMPTY_STRING;
    } else {
      trimmed = line.substring(start);
    }

    if (prefix == null) {
      return trimmed;
    }
    return prefix + trimmed;
  }

  /*
   * Calculates space equivalents up to the next tab stop
   */
  private static int calculateSpaceEquivalents(int tabWidth, int spaceEquivalents) {
    if (tabWidth == 0) {
      return spaceEquivalents;
    }
    int remainder = spaceEquivalents % tabWidth;
    spaceEquivalents += tabWidth - remainder;
    return spaceEquivalents;
  }

  private static int getIntValue(Map<String, String> options, String key, int def) {
    try {
      return Integer.parseInt(options.get(key));
    } catch (NumberFormatException e) {
      return def;
    }
  }

  /*
   * Returns the index where the indent of the given size ends. Returns <code>-1<code> if the line
   * isn't prefixed with an indent of the given number of indents.
   */
  private static int indexOfIndent(CharSequence line, int numberOfIndentUnits, int tabWidth,
      int indentWidth) {

    int spaceEquivalents = numberOfIndentUnits * indentWidth;

    int size = line.length();
    int result = -1;
    int blanks = 0;
    for (int i = 0; i < size && blanks < spaceEquivalents; i++) {
      char c = line.charAt(i);
      if (c == '\t') {
        blanks = calculateSpaceEquivalents(tabWidth, blanks);
      } else if (isIndentChar(c)) {
        blanks++;
      } else {
        break;
      }
      result = i;
    }
    if (blanks < spaceEquivalents) {
      return -1;
    }
    return result + 1;
  }

  private IndentManipulation() {
    // don't instantiate
  }
}
